Проект: Market one click¶
Описание проекта
Интернет-магазин «В один клик» продаёт разные товары: для детей, для дома, мелкую бытовую технику, косметику и даже продукты. Отчёт магазина за прошлый период показал, что активность покупателей начала снижаться.
Привлекать новых клиентов уже не так эффективно: о магазине и так знает большая часть целевой аудитории.
Возможный выход — удерживать активность постоянных клиентов.
Сделать это можно с помощью персонализированных предложений.
Поставленная задача
Разработать модель, помогающую удерживать персональных клиентов с помощью персонализированных предложений.
Описание данных
market_file.csv - Таблица, которая содержит данные о поведении покупателя на сайте, о коммуникациях с покупателем и его продуктовом поведении.
- id — номер покупателя в корпоративной базе данных.
- Покупательская активность — рассчитанный класс покупательской активности (целевой признак): «снизилась» или «прежний уровень».
- Тип сервиса — уровень сервиса, например «премиум» и «стандарт».
- Разрешить сообщать — информация о том, можно ли присылать покупателю дополнительные предложения о товаре. Согласие на это даёт покупатель.
- Маркет_актив_6_мес — среднемесячное значение маркетинговых коммуникаций компании, которое приходилось на покупателя за последние 6 месяцев. Это значение показывает, какое число рассылок, звонков, показов рекламы и прочего приходилось на клиента.
- Маркет_актив_тек_мес — количество маркетинговых коммуникаций в текущем месяце.
- Длительность — значение, которое показывает, сколько дней прошло с момента регистрации покупателя на сайте.
- Акционные_покупки — среднемесячная доля покупок по акции от общего числа покупок за последние 6 месяцев.
- Популярная_категория — самая популярная категория товаров у покупателя за последние 6 месяцев.
- Средний_просмотр_категорий_за_визит — показывает, сколько в среднем категорий покупатель просмотрел за визит в течение последнего месяца.
- Неоплаченные_продукты_штук_квартал — общее число неоплаченных товаров в корзине за последние 3 месяца.
- Ошибка_сервиса — число сбоев, которые коснулись покупателя во время посещения сайта.
- Страниц_за_визит — среднее количество страниц, которые просмотрел покупатель за один визит на сайт за последние 3 месяца.
market_money.csv - Таблица с данными о выручке, которую получает магазин с покупателя, то есть сколько покупатель всего потратил за период взаимодействия с сайтом.
- id — номер покупателя в корпоративной базе данных.
- Период — название периода, во время которого зафиксирована выручка. Например, 'текущий_месяц' или 'предыдущий_месяц'.
- Выручка — сумма выручки за период.
market_time.csv - Таблица с данными о времени (в минутах), которое покупатель провёл на сайте в течение периода.
- id — номер покупателя в корпоративной базе данных.
- Период — название периода, во время которого зафиксировано общее время.
- минут — значение времени, проведённого на сайте, в минутах.
money.csv - Таблица с данными о среднемесячной прибыли покупателя за последние 3 месяца: какую прибыль получает магазин от продаж каждому покупателю.
- id — номер покупателя в корпоративной базе данных.
- Прибыль — значение прибыли.
Импортирование необходимых библиотек
%%capture
%pip install -U scikit-learn
!pip -q install phik
!pip -q install shap
# !pip -q install pandas
!pip -q install openpyxl
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import re
import seaborn as sns
import shap
import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)
from IPython.display import display
from phik import phik_matrix
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import (
StandardScaler, OneHotEncoder,
MinMaxScaler, LabelEncoder, OrdinalEncoder
)
from sklearn.impute import SimpleImputer
from sklearn.model_selection import train_test_split, RandomizedSearchCV, GridSearchCV
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import SVC
from sklearn.metrics import roc_auc_score
from scipy.stats import shapiro
# Настройки вывода графиков
plt.rcParams["axes.titlesize"] = 16 # Размер шрифта
plt.rcParams["axes.titleweight"] = "bold" # Толщина шрифта
# Константы
RANDOM_STATE = 42
TEST_SIZE = 0.25
TERM_SIZE = 180
Загрузка данных¶
try:
market_file = pd.read_csv("C:\\Data-science\\ds_csv\\market_file.csv")
market_money = pd.read_csv("C:\\Data-science\\ds_csv\\market_money.csv")
market_time = pd.read_csv("C:\\Data-science\\ds_csv\\market_time.csv")
money = pd.read_csv("C:\\Data-science\\ds_csv\\money.csv", sep=';', decimal=",")
except:
try:
market_file = pd.read_csv('/datasets/market_file.csv')
market_money = pd.read_csv('/datasets/market_money.csv')
market_time = pd.read_csv('/datasets/market_time.csv')
money = pd.read_csv('/datasets/money.csv', sep=';', decimal=",")
except:
raise FileNotFoundError
Изучение загруженных датасетов¶
# Создаем словарь, чтобы перебрать все импортируемые датафреймы
dataframes = {
"market_file": market_file,
"market_money": market_money,
"market_time": market_time,
"money": money
}
# Цикл по каждому DataFrame с выводом имени, информации и первых строк
for name, data in dataframes.items():
print(f'\033[1mНаименование анализируемого датафрейма:\033[0m {name}')
print()
data.info() # Выводим информацию о DataFrame
display(data.head(5)) # Отображаем первые 5 строк
print('=' * TERM_SIZE) # Линия-разделитель по ширине терминала
Наименование анализируемого датафрейма: market_file
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1300 entries, 0 to 1299
Data columns (total 13 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 1300 non-null int64
1 Покупательская активность 1300 non-null object
2 Тип сервиса 1300 non-null object
3 Разрешить сообщать 1300 non-null object
4 Маркет_актив_6_мес 1300 non-null float64
5 Маркет_актив_тек_мес 1300 non-null int64
6 Длительность 1300 non-null int64
7 Акционные_покупки 1300 non-null float64
8 Популярная_категория 1300 non-null object
9 Средний_просмотр_категорий_за_визит 1300 non-null int64
10 Неоплаченные_продукты_штук_квартал 1300 non-null int64
11 Ошибка_сервиса 1300 non-null int64
12 Страниц_за_визит 1300 non-null int64
dtypes: float64(2), int64(7), object(4)
memory usage: 132.2+ KB
| id | Покупательская активность | Тип сервиса | Разрешить сообщать | Маркет_актив_6_мес | Маркет_актив_тек_мес | Длительность | Акционные_покупки | Популярная_категория | Средний_просмотр_категорий_за_визит | Неоплаченные_продукты_штук_квартал | Ошибка_сервиса | Страниц_за_визит | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 215348 | Снизилась | премиум | да | 3.4 | 5 | 121 | 0.00 | Товары для детей | 6 | 2 | 1 | 5 |
| 1 | 215349 | Снизилась | премиум | да | 4.4 | 4 | 819 | 0.75 | Товары для детей | 4 | 4 | 2 | 5 |
| 2 | 215350 | Снизилась | стандартт | нет | 4.9 | 3 | 539 | 0.14 | Домашний текстиль | 5 | 2 | 1 | 5 |
| 3 | 215351 | Снизилась | стандартт | да | 3.2 | 5 | 896 | 0.99 | Товары для детей | 5 | 0 | 6 | 4 |
| 4 | 215352 | Снизилась | стандартт | нет | 5.1 | 3 | 1064 | 0.94 | Товары для детей | 3 | 2 | 3 | 2 |
====================================================================================================================================================================================
Наименование анализируемого датафрейма: market_money
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3900 entries, 0 to 3899
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 3900 non-null int64
1 Период 3900 non-null object
2 Выручка 3900 non-null float64
dtypes: float64(1), int64(1), object(1)
memory usage: 91.5+ KB
| id | Период | Выручка | |
|---|---|---|---|
| 0 | 215348 | препредыдущий_месяц | 0.0 |
| 1 | 215348 | текущий_месяц | 3293.1 |
| 2 | 215348 | предыдущий_месяц | 0.0 |
| 3 | 215349 | препредыдущий_месяц | 4472.0 |
| 4 | 215349 | текущий_месяц | 4971.6 |
====================================================================================================================================================================================
Наименование анализируемого датафрейма: market_time
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2600 entries, 0 to 2599
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 2600 non-null int64
1 Период 2600 non-null object
2 минут 2600 non-null int64
dtypes: int64(2), object(1)
memory usage: 61.1+ KB
| id | Период | минут | |
|---|---|---|---|
| 0 | 215348 | текущий_месяц | 14 |
| 1 | 215348 | предыдцщий_месяц | 13 |
| 2 | 215349 | текущий_месяц | 10 |
| 3 | 215349 | предыдцщий_месяц | 12 |
| 4 | 215350 | текущий_месяц | 13 |
====================================================================================================================================================================================
Наименование анализируемого датафрейма: money
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1300 entries, 0 to 1299
Data columns (total 2 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 1300 non-null int64
1 Прибыль 1300 non-null float64
dtypes: float64(1), int64(1)
memory usage: 20.4 KB
| id | Прибыль | |
|---|---|---|
| 0 | 215348 | 0.98 |
| 1 | 215349 | 4.16 |
| 2 | 215350 | 3.13 |
| 3 | 215351 | 4.87 |
| 4 | 215352 | 4.21 |
====================================================================================================================================================================================
# установка индекса id датафрейма id покупателя
dfs = [
market_file,
market_money,
market_time,
money
]
for df in dfs:
df = df.set_index('id')
# Проверка
display(df.head(1))
| Покупательская активность | Тип сервиса | Разрешить сообщать | Маркет_актив_6_мес | Маркет_актив_тек_мес | Длительность | Акционные_покупки | Популярная_категория | Средний_просмотр_категорий_за_визит | Неоплаченные_продукты_штук_квартал | Ошибка_сервиса | Страниц_за_визит | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| id | ||||||||||||
| 215348 | Снизилась | премиум | да | 3.4 | 5 | 121 | 0.0 | Товары для детей | 6 | 2 | 1 | 5 |
| Период | Выручка | |
|---|---|---|
| id | ||
| 215348 | препредыдущий_месяц | 0.0 |
| Период | минут | |
|---|---|---|
| id | ||
| 215348 | текущий_месяц | 14 |
| Прибыль | |
|---|---|
| id | |
| 215348 | 0.98 |
Вывод по предварительному анализу:
В ходе предварительного анализа данных было выявлено:
- В датафреймах отсвутствуют пропуски;
- Название столбцов датафреймов необходимо привести к snake_case;
- Нет ошибок в типах данных в датафреймах;
- Столбцы id в датафреймах сделали значениями индексов;
- Была получена общая информация о датафреймах.
Предобработка данных¶
Приведение названий столбцов датафреймов к snake_case¶
# приведение названий столбцов датафреймов в snake_case
for df in dfs:
df.columns = [re.sub(r'(?<!^)(?=[A-Z])', '_', i). replace(' ', '_').lower() for i in df.columns]
# проверка
for df in dfs:
df.info()
print('=' * TERM_SIZE)
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1300 entries, 0 to 1299 Data columns (total 13 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 1300 non-null int64 1 покупательская_активность 1300 non-null object 2 тип_сервиса 1300 non-null object 3 разрешить_сообщать 1300 non-null object 4 маркет_актив_6_мес 1300 non-null float64 5 маркет_актив_тек_мес 1300 non-null int64 6 длительность 1300 non-null int64 7 акционные_покупки 1300 non-null float64 8 популярная_категория 1300 non-null object 9 средний_просмотр_категорий_за_визит 1300 non-null int64 10 неоплаченные_продукты_штук_квартал 1300 non-null int64 11 ошибка_сервиса 1300 non-null int64 12 страниц_за_визит 1300 non-null int64 dtypes: float64(2), int64(7), object(4) memory usage: 132.2+ KB ==================================================================================================================================================================================== <class 'pandas.core.frame.DataFrame'> RangeIndex: 3900 entries, 0 to 3899 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 3900 non-null int64 1 период 3900 non-null object 2 выручка 3900 non-null float64 dtypes: float64(1), int64(1), object(1) memory usage: 91.5+ KB ==================================================================================================================================================================================== <class 'pandas.core.frame.DataFrame'> RangeIndex: 2600 entries, 0 to 2599 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 2600 non-null int64 1 период 2600 non-null object 2 минут 2600 non-null int64 dtypes: int64(2), object(1) memory usage: 61.1+ KB ==================================================================================================================================================================================== <class 'pandas.core.frame.DataFrame'> RangeIndex: 1300 entries, 0 to 1299 Data columns (total 2 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 1300 non-null int64 1 прибыль 1300 non-null float64 dtypes: float64(1), int64(1) memory usage: 20.4 KB ====================================================================================================================================================================================
Проверка датафреймов на дубликаты¶
Проверка на явные дубликаты
# Обновление словаря
dataframes = {
"market_file": market_file,
"market_money": market_money,
"market_time": market_time,
"money": money
}
for name, data in dataframes.items():
print(f"Явных дубликатов в датасете {name} - {data.duplicated().sum()}: "
if data.duplicated().sum() > 0 else f"Явных дубликатов в датасете {name} - НЕТ")
Явных дубликатов в датасете market_file - НЕТ Явных дубликатов в датасете market_money - НЕТ Явных дубликатов в датасете market_time - НЕТ Явных дубликатов в датасете money - НЕТ
Проверка на неявные дубликаты
for name, data in dataframes.items():
col_cat = data.select_dtypes(include=['object']).columns.to_list()
print(f'Название датафрейма {name}')
for col in col_cat:
print(f"Уникальные значения в столбце '{col}': {data[col].unique()}")
print('=' * TERM_SIZE)
Название датафрейма market_file Уникальные значения в столбце 'покупательская_активность': ['Снизилась' 'Прежний уровень'] Уникальные значения в столбце 'тип_сервиса': ['премиум' 'стандартт' 'стандарт'] Уникальные значения в столбце 'разрешить_сообщать': ['да' 'нет'] Уникальные значения в столбце 'популярная_категория': ['Товары для детей' 'Домашний текстиль' 'Косметика и аксесуары' 'Техника для красоты и здоровья' 'Кухонная посуда' 'Мелкая бытовая техника и электроника'] ==================================================================================================================================================================================== Название датафрейма market_money Уникальные значения в столбце 'период': ['препредыдущий_месяц' 'текущий_месяц' 'предыдущий_месяц'] ==================================================================================================================================================================================== Название датафрейма market_time Уникальные значения в столбце 'период': ['текущий_месяц' 'предыдцщий_месяц'] ==================================================================================================================================================================================== Название датафрейма money ====================================================================================================================================================================================
# Переименование дубликатов
market_file.loc[market_file['тип_сервиса'] == 'стандартт', 'тип_сервиса'] = 'стандарт'
market_time.loc[market_time['период'] == 'предыдцщий_месяц', 'период'] = 'предыдущий_месяц'
# Проверка
display(market_file['тип_сервиса'].unique())
market_time['период'].unique()
array(['премиум', 'стандарт'], dtype=object)
array(['текущий_месяц', 'предыдущий_месяц'], dtype=object)
# Обновление словаря
dataframes = {
"market_file": market_file,
"market_money": market_money,
"market_time": market_time,
"money": money
}
for name, data in dataframes.items():
print(f"Явных дубликатов в датасете {name} - {data.duplicated().sum()}: "
if data.duplicated().sum() > 0 else f"Явных дубликатов в датасете {name} - НЕТ")
Явных дубликатов в датасете market_file - НЕТ Явных дубликатов в датасете market_money - НЕТ Явных дубликатов в датасете market_time - НЕТ Явных дубликатов в датасете money - НЕТ
Вывод по предобработке данных:
В ходе предобработке данных было выполнено:
- Приведены названия столбцов датафреймов к snake_case;
- Проверены датафреймы на дубликаты и выявлено их отсутствие.
Исследовательский анализ данных¶
Функции для исследовательского анализа данных¶
def show_num_variable(df, column, target=None):
'''
Функция отображения гистограммы распределения
и диаграммы размаха для определенного столбца датафрейма
с учетом принадлежности данного столбца к разным значениям
переменной target.
Параметры:
- df: pandas.DataFrame, входной датафрейм
- column: str, столбец для анализа
- target: str или None, столбец для группировки (по умолчанию None)
'''
sns.set()
f, axes = plt.subplots(1, 2, figsize=(16, 6))
# Гистограмма
axes[0].set_title(f'Гистограмма для {column}', fontsize=16)
axes[0].set_ylabel('Количество', fontsize=14)
if target:
sns.histplot(data=df, bins=20, kde=True, ax=axes[0], hue=target, x=column)
else:
sns.histplot(data=df, bins=20, kde=True, ax=axes[0], x=column)
# Диаграмма размаха
axes[1].set_title(f'Диаграмма размаха для {column}', fontsize=16)
if target:
sns.boxplot(data=df, ax=axes[1], x=target, y=column)
else:
sns.boxplot(data=df, ax=axes[1], y=column, orient='v')
axes[1].set_ylabel(column, fontsize=14)
plt.tight_layout()
plt.show()
def show_cat_variable_by_target(df, column, title, target=None, rot=60):
'''
Функция отображения соотношения категориальных признаков
в столбце датафрейма, разделенных по значениям целевого признака.
Если target не передан, отображается только countplot для column.
Добавляет проценты на график.
:param df: DataFrame, датафрейм с данными
:param column: str, название столбца, по которому строится график
:param title: str, заголовок графика
:param target: str, название столбца с целевым признаком (по умолчанию None)
:param rot: int, угол поворота меток на оси X
'''
# Если target не указан, рисуем только countplot для одного признака
if target is None:
plt.figure(figsize=(12, 6))
ax = sns.countplot(data=df, x=column)
ax.set_title(f"{title}", fontsize=14)
ax.set_xlabel(column, fontsize=12)
ax.set_ylabel('Количество', fontsize=12)
ax.tick_params(axis='x', rotation=rot)
# Добавляем проценты на график
total_count = len(df)
for p in ax.patches:
count = p.get_height()
if count > 0: # Добавляем проценты только если высота столбца > 0
percentage = f'{100 * count / total_count:.1f}%'
ax.annotate(percentage,
(p.get_x() + p.get_width() / 2., count),
ha='center', va='center',
xytext=(0, 10),
textcoords='offset points',
fontsize=12, color='black')
plt.tight_layout()
plt.show()
return
# Проверяем, что столбец target существует в DataFrame
if target not in df.columns:
raise ValueError(f"Столбец '{target}' не найден в DataFrame.")
# Если target указан, строим графики для каждого уникального значения target
unique_targets = df[target].unique()
num_targets = len(unique_targets)
# Определяем размер холста в зависимости от количества таргетов
fig, axes = plt.subplots(nrows=num_targets, ncols=1, figsize=(12, 5 * num_targets), sharex=True)
# Если уникальных значений больше одного, axes будет массивом
# Если только одно значение, делаем axes списком для унификации
if num_targets == 1:
axes = [axes]
# Создаем график для каждого уникального значения целевого признака
for i, target_value in enumerate(unique_targets):
# Создаем подмножество данных для текущего значения целевого признака
subset = df[df[target] == target_value]
# Получаем уникальные значения категориального признака для сортировки
categories = subset[column].value_counts().index.tolist()
# Создаем countplot для текущего значения целевого признака
ax = sns.countplot(data=subset, x=column, ax=axes[i], order=categories)
# Заголовок и настройка осей
axes[i].set_title(f"{title}: {target} - {target_value}", fontsize=14)
axes[i].set_xlabel(column if i == num_targets - 1 else "", fontsize=12) # Подпись только для нижнего графика
axes[i].set_ylabel('Количество', fontsize=12)
axes[i].tick_params(axis='x', rotation=rot)
# Вычисляем общее количество для текущей группы
total_count = len(subset)
# Добавляем проценты на столбцы
for p in ax.patches:
count = p.get_height()
if count > 0: # Добавляем проценты только если высота столбца > 0
percentage = f'{100 * count / total_count:.1f}%'
# Вычисляем координаты для аннотации (верхняя часть столбца)
ax.annotate(percentage,
(p.get_x() + p.get_width() / 2., count),
ha='center', va='center',
xytext=(0, 10),
textcoords='offset points',
fontsize=12, color='black')
# Улучшаем отображение
plt.tight_layout(h_pad=2.0) # Добавляем вертикальный отступ между графиками
plt.show()
def normal_check(data, column, alpha=0.05):
'''
Функция проверки нормальности распределения
по тесту Шапиро — Уилка
'''
stat, p = shapiro(data[column])
print(f"Тест Шапиро — Уилка: Stat={stat}, p={p}")
# Результат
if p > alpha:
return print(f"Распределение данных нормальное с вероятностью более {1 - alpha}.")
else:
return print(f"Распределение данных не нормальное с вероятностью более {1 - alpha}.")
Анализ количественных и качественных признаков¶
Датафрейм market_file
# Формирование списка столбцов с количественными признаками
num_variables_col = market_file.select_dtypes(include=['number']).columns.to_list()
num_variables_col.remove('id')
num_variables_col
['маркет_актив_6_мес', 'маркет_актив_тек_мес', 'длительность', 'акционные_покупки', 'средний_просмотр_категорий_за_визит', 'неоплаченные_продукты_штук_квартал', 'ошибка_сервиса', 'страниц_за_визит']
# Вывод графиков для датафрейма market_file
for col in num_variables_col:
show_num_variable(market_file, col, 'покупательская_активность')
print('=' * TERM_SIZE)
====================================================================================================================================================================================
====================================================================================================================================================================================
====================================================================================================================================================================================
====================================================================================================================================================================================
====================================================================================================================================================================================
====================================================================================================================================================================================
====================================================================================================================================================================================
====================================================================================================================================================================================
stats_df = market_file.groupby('покупательская_активность')[num_variables_col].describe().round(3).T
try:
stats_df.to_excel('output.xlsx')
except:
display(stats_df)
# Проверка на нормальность распределения с помощью теста Шапиро
for group in market_file['покупательская_активность'].unique():
print(f"\033[1mПроверка нормальности для группы {group}\033[0m:")
for col in num_variables_col:
print(f'\033[1mДля столбца:\033[0m {col}')
normal_check(market_file[market_file['покупательская_активность'] == group], col)
print('=' * TERM_SIZE)
Проверка нормальности для группы Снизилась: Для столбца: маркет_актив_6_мес Тест Шапиро — Уилка: Stat=0.9748254418373108, p=1.480918996321634e-07 Распределение данных не нормальное с вероятностью более 0.95. ==================================================================================================================================================================================== Для столбца: маркет_актив_тек_мес Тест Шапиро — Уилка: Stat=0.8077908754348755, p=6.791739773128278e-24 Распределение данных не нормальное с вероятностью более 0.95. ==================================================================================================================================================================================== Для столбца: длительность Тест Шапиро — Уилка: Stat=0.9783022403717041, p=9.273203431803267e-07 Распределение данных не нормальное с вероятностью более 0.95. ==================================================================================================================================================================================== Для столбца: акционные_покупки Тест Шапиро — Уилка: Stat=0.7598561644554138, p=2.6357747202963015e-26 Распределение данных не нормальное с вероятностью более 0.95. ==================================================================================================================================================================================== Для столбца: средний_просмотр_категорий_за_визит Тест Шапиро — Уилка: Stat=0.8931688070297241, p=3.993350478310506e-18 Распределение данных не нормальное с вероятностью более 0.95. ==================================================================================================================================================================================== Для столбца: неоплаченные_продукты_штук_квартал Тест Шапиро — Уилка: Stat=0.9590765237808228, p=1.5509174500216716e-10 Распределение данных не нормальное с вероятностью более 0.95. ==================================================================================================================================================================================== Для столбца: ошибка_сервиса Тест Шапиро — Уилка: Stat=0.9374690651893616, p=1.3106536526658746e-13 Распределение данных не нормальное с вероятностью более 0.95. ==================================================================================================================================================================================== Для столбца: страниц_за_визит Тест Шапиро — Уилка: Stat=0.8759505152702332, p=1.639263240222402e-19 Распределение данных не нормальное с вероятностью более 0.95. ==================================================================================================================================================================================== Проверка нормальности для группы Прежний уровень: Для столбца: маркет_актив_6_мес Тест Шапиро — Уилка: Stat=0.9850596189498901, p=2.698477317153447e-07 Распределение данных не нормальное с вероятностью более 0.95. ==================================================================================================================================================================================== Для столбца: маркет_актив_тек_мес Тест Шапиро — Уилка: Stat=0.8043734431266785, p=5.030570392140609e-30 Распределение данных не нормальное с вероятностью более 0.95. ==================================================================================================================================================================================== Для столбца: длительность Тест Шапиро — Уилка: Stat=0.9670731425285339, p=1.8170366470129928e-12 Распределение данных не нормальное с вероятностью более 0.95. ==================================================================================================================================================================================== Для столбца: акционные_покупки Тест Шапиро — Уилка: Stat=0.5570206642150879, p=7.862685683326549e-41 Распределение данных не нормальное с вероятностью более 0.95. ==================================================================================================================================================================================== Для столбца: средний_просмотр_категорий_за_визит Тест Шапиро — Уилка: Stat=0.9318152070045471, p=1.1972408492866681e-18 Распределение данных не нормальное с вероятностью более 0.95. ==================================================================================================================================================================================== Для столбца: неоплаченные_продукты_штук_квартал Тест Шапиро — Уилка: Stat=0.9326432943344116, p=1.5603153389521451e-18 Распределение данных не нормальное с вероятностью более 0.95. ==================================================================================================================================================================================== Для столбца: ошибка_сервиса Тест Шапиро — Уилка: Stat=0.9724842309951782, p=3.7529094415456044e-11 Распределение данных не нормальное с вероятностью более 0.95. ==================================================================================================================================================================================== Для столбца: страниц_за_визит Тест Шапиро — Уилка: Stat=0.980556070804596, p=7.650830191607838e-09 Распределение данных не нормальное с вероятностью более 0.95. ====================================================================================================================================================================================
Вывод по полученным графикам количественных признаков в датафрейме market_file:
- Распределения всех количественных признаков в датафрейме
market_fileотличаются от Гауссовского; - Поведение клиентов с разным целевым признаком -
покупательской_активностьотличаются:- Покупательская активность в среднем выше для тех клиентов, у которых больший показатель
маркет_актив_6_мес(покупательская_активность - Прежний уровень:mean = 4.57 и median = 4.4,покупательская_активность - Снизилась:mean = 4.57 и median = 4.4), что может указывать на большую вовлеченность тех клиентов, которым чаще приходит рассылка; - Клиенты, чья активность снизилась, в среднем проводят больше времени, на что указывает среднее и медианное значение показателя
длительность(покупательская_активность - Прежний уровень:mean = 590 и median = 590,покупательская_активность - Снизилась:mean = 620 и median = 634); - Признак
акционные_покупкираспределен бимодально, с явным разделением на две категории (1-я <= 0.65, 2-я > 0.65). Поэтому для подготовки данных к модели целесообразно разделить пользователей на две группы:часто покупает по акциииредко покупает по акции, преобразовав колонкуАкционные_покупкив категориальный признак; покупательской_активностьтакже положительно влияет на количество просмотренных страниц и категорий,средний_просмотр_категорий_за_визит(покупательская_активность - Прежний уровень:mean = 3.67 и median = 4,покупательская_активность - Снизилась:mean = 2.63 и median = 2),страниц_за_визит(покупательская_активность - Прежний уровень:mean = 9.8 и median = 10,покупательская_активность - Снизилась:mean = 5.57 и median = 5);- Значение признака
ошибка_сервисапрактически не зависит отпокупательской_активность(покупательская_активность - Прежний уровень:mean = 4.3 и median = 4,покупательская_активность - Снизилась:mean = 3.94 и median = 4); - Значение признака
неоплаченные_продукты_штук_кварталниже у активных клиентов (покупательская_активность - Прежний уровень:mean = 2.23 и median = 2,покупательская_активность - Снизилась:mean = 3.72 и median = 4).
- Покупательская активность в среднем выше для тех клиентов, у которых больший показатель
market_file.groupby('покупательская_активность').describe(include='object').round(3).T
| покупательская_активность | Прежний уровень | Снизилась | |
|---|---|---|---|
| тип_сервиса | count | 802 | 498 |
| unique | 2 | 2 | |
| top | стандарт | стандарт | |
| freq | 596 | 328 | |
| разрешить_сообщать | count | 802 | 498 |
| unique | 2 | 2 | |
| top | да | да | |
| freq | 591 | 371 | |
| популярная_категория | count | 802 | 498 |
| unique | 6 | 6 | |
| top | Товары для детей | Товары для детей | |
| freq | 184 | 146 |
show_cat_variable_by_target(market_file, 'покупательская_активность', 'покупательская_активность')
# Лист с названиями столбцов категориальных признаков
dict_market_file_cat = market_file.select_dtypes(include=['object']).columns.to_list()
dict_market_file_cat.remove('покупательская_активность')
for col in dict_market_file_cat:
show_cat_variable_by_target(market_file, col, col, 'покупательская_активность')
print('=' * TERM_SIZE)
====================================================================================================================================================================================
====================================================================================================================================================================================
====================================================================================================================================================================================
Вывод по полученным графикам категориальных признаков датафрейма market_file:
Целевой признак покупательская_активность не привносит существенный вклад на категориальные признаки;
Целевой признак покупательская_активность разделен на две категории "Снизилась" 38.3 % наблюдений и "Прежний уровень" - 61.7 % наблюдения;
Наблюдается дисбаланс в биномиальных категориальных признаках: тип_сервиса и разрешить_сообщать;
Признак популярная_категория мультиклассовый, наиболее популярный класс - Товары для детей (25.4 %), а наименее популярный Кухонная посуда (10.6 %).
Датафрейм market_money
show_num_variable(market_money, 'выручка', 'период')
emissions_id = market_money.query('выручка > 8000')['id']
emissions_id
98 215380 Name: id, dtype: int64
market_file.query('id in @emissions_id')
| id | покупательская_активность | тип_сервиса | разрешить_сообщать | маркет_актив_6_мес | маркет_актив_тек_мес | длительность | акционные_покупки | популярная_категория | средний_просмотр_категорий_за_визит | неоплаченные_продукты_штук_квартал | ошибка_сервиса | страниц_за_визит | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 32 | 215380 | Снизилась | премиум | нет | 1.7 | 4 | 637 | 0.94 | Техника для красоты и здоровья | 3 | 2 | 4 | 7 |
Замечен единичный выброс для суммы выручки за текущий месяц, уберем его
market_money = market_money.query('выручка < 8000')
market_money.groupby('период')['выручка'].describe().round(3).T
| период | предыдущий_месяц | препредыдущий_месяц | текущий_месяц |
|---|---|---|---|
| count | 1300.000 | 1300.000 | 1299.000 |
| mean | 4936.920 | 4825.207 | 5236.787 |
| std | 739.598 | 405.980 | 835.475 |
| min | 0.000 | 0.000 | 2758.700 |
| 25% | 4496.750 | 4583.000 | 4705.500 |
| 50% | 5005.000 | 4809.000 | 5179.600 |
| 75% | 5405.625 | 5053.500 | 5759.950 |
| max | 6869.500 | 5663.000 | 7799.400 |
show_num_variable(market_money, 'выручка', 'период')
for group in market_money['период'].unique():
print(f"\033[1mПроверка нормальности для группы {group}\033[0m:")
print(f'\033[1mДля столбца:\033[0m {"выручка"}')
normal_check(market_money[market_money['период'] == group], 'выручка')
print('=' * TERM_SIZE)
Проверка нормальности для группы препредыдущий_месяц: Для столбца: выручка Тест Шапиро — Уилка: Stat=0.7978613376617432, p=1.993647055477295e-37 Распределение данных не нормальное с вероятностью более 0.95. ==================================================================================================================================================================================== Проверка нормальности для группы текущий_месяц: Для столбца: выручка Тест Шапиро — Уилка: Stat=0.9947782158851624, p=0.00017225794726982713 Распределение данных не нормальное с вероятностью более 0.95. ==================================================================================================================================================================================== Проверка нормальности для группы предыдущий_месяц: Для столбца: выручка Тест Шапиро — Уилка: Stat=0.9597474336624146, p=1.7770246653221564e-18 Распределение данных не нормальное с вероятностью более 0.95. ====================================================================================================================================================================================
show_cat_variable_by_target(market_money, "период", "период", rot=0)
Вывод по анализу графиков для датафрейма market_money
- Категориальный признак
периодраспределен равномерно, дисбаланс отсутствует; - Выручка схожа для разных периодов, минимальная десперсия выручки у периода препредыдущий_месяц.
market_time
market_time.groupby('период')['минут'].describe().round(3).T
| период | предыдущий_месяц | текущий_месяц |
|---|---|---|
| count | 1300.000 | 1300.000 |
| mean | 13.468 | 13.205 |
| std | 3.932 | 4.221 |
| min | 5.000 | 4.000 |
| 25% | 11.000 | 10.000 |
| 50% | 13.000 | 13.000 |
| 75% | 17.000 | 16.000 |
| max | 23.000 | 23.000 |
show_num_variable(market_time, 'минут', 'период')
for group in market_time['период'].unique():
print(f"\033[1mПроверка нормальности для группы {group}\033[0m:")
print(f'\033[1mДля столбца:\033[0m {"минут"}')
normal_check(market_time[market_time['период'] == group], 'минут')
print('=' * TERM_SIZE)
Проверка нормальности для группы текущий_месяц: Для столбца: минут Тест Шапиро — Уилка: Stat=0.9797751903533936, p=1.5868249639630627e-12 Распределение данных не нормальное с вероятностью более 0.95. ==================================================================================================================================================================================== Проверка нормальности для группы предыдущий_месяц: Для столбца: минут Тест Шапиро — Уилка: Stat=0.9828875660896301, p=2.845738401868747e-11 Распределение данных не нормальное с вероятностью более 0.95. ====================================================================================================================================================================================
show_cat_variable_by_target(market_time, "период", "период", rot=0)
Вывод по анализу графиков для датафрейма market_time
- Дисбаланса категориальных признаков в датафрейме не замечено;
- Отличие проведенного времяни на сайте в разные периоды времени не замечено.
money
money.describe().round(3)
| id | прибыль | |
|---|---|---|
| count | 1300.000 | 1300.000 |
| mean | 215997.500 | 3.997 |
| std | 375.422 | 1.014 |
| min | 215348.000 | 0.860 |
| 25% | 215672.750 | 3.300 |
| 50% | 215997.500 | 4.045 |
| 75% | 216322.250 | 4.670 |
| max | 216647.000 | 7.430 |
show_num_variable(money, 'прибыль')
normal_check(money, 'прибыль')
Тест Шапиро — Уилка: Stat=0.9983819723129272, p=0.25812551379203796 Распределение данных нормальное с вероятностью более 0.95.
Признак прибыль распределен нормально. mean = 3.997 и median = 4.045
Выбор покупателей, активных в последние 3 месяца¶
Покупателя можно считать активным, если выручка за его деятельность во всех 3-х месяцах больше 0. Т.е. нам нужны записи из market_money, где у клиента есть покупки за все три периода 'препредыдущий_месяц', 'текущий_месяц', 'предыдущий_месяц'.
# преобразование данных
market_money = market_money.pivot(index='id', columns='период', values='выручка')
market_money.columns = ['выручка_' + str(col) for col in market_money.columns]
# упорядочивание столбцов
market_money = market_money[['выручка_препредыдущий_месяц', 'выручка_предыдущий_месяц', 'выручка_текущий_месяц']]
# фильтрация данных
market_money = market_money[(market_money['выручка_препредыдущий_месяц'] > 0) & \
(market_money['выручка_предыдущий_месяц'] > 0) & (market_money['выручка_текущий_месяц'] > 0)]
display(market_money.sample(5))
market_money.shape
| выручка_препредыдущий_месяц | выручка_предыдущий_месяц | выручка_текущий_месяц | |
|---|---|---|---|
| id | |||
| 215400 | 4439.0 | 5681.0 | 5691.4 |
| 215784 | 4903.0 | 4165.0 | 3655.4 |
| 216217 | 4703.0 | 4091.0 | 3690.6 |
| 216125 | 5235.0 | 5358.0 | 5594.6 |
| 215881 | 4786.0 | 5084.0 | 4955.3 |
(1296, 3)
Общий вывод по исследовательскому анализу данных:
В ходе исследовательского анализа было замечено влияние целевого признака покупательская_актиивность на остальные.
Анализ данных показал, что большинство пользователей выбирает стандартный тип сервиса, разрешать_сообщать = да и предпочитает раздел «Товары для детей».
Пользователи, чья активность снизилась, в среднем проводят больше времени на сайте и совершают больше покупок со скидками. Возможно, это связано с тем, что они ищут более выгодные предложения или скидки, что указывает на их чувствительность к ценам. Если это действительно так, то предоставление специальных акций или скидок этой группе пользователей может повысить их активность и прибыль.
Объединение таблиц¶
# преобразование данных
market_time = market_time.pivot(index='id', columns='период', values='минут')
market_time.columns = ['минут_' + str(col) for col in market_time.columns]
display(market_time)
| минут_предыдущий_месяц | минут_текущий_месяц | |
|---|---|---|
| id | ||
| 215348 | 13 | 14 |
| 215349 | 12 | 10 |
| 215350 | 8 | 13 |
| 215351 | 11 | 13 |
| 215352 | 8 | 11 |
| ... | ... | ... |
| 216643 | 14 | 7 |
| 216644 | 12 | 11 |
| 216645 | 12 | 18 |
| 216646 | 18 | 7 |
| 216647 | 15 | 10 |
1300 rows × 2 columns
# объединение данных
market_full = market_file.merge(market_money, on='id', how='inner')
market_full = market_full.merge(market_time, on='id', how='inner')
# Проверка
display(market_full.sample(5))
market_full.shape
| id | покупательская_активность | тип_сервиса | разрешить_сообщать | маркет_актив_6_мес | маркет_актив_тек_мес | длительность | акционные_покупки | популярная_категория | средний_просмотр_категорий_за_визит | неоплаченные_продукты_штук_квартал | ошибка_сервиса | страниц_за_визит | выручка_препредыдущий_месяц | выручка_предыдущий_месяц | выручка_текущий_месяц | минут_предыдущий_месяц | минут_текущий_месяц | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 93 | 215445 | Снизилась | стандарт | нет | 4.0 | 5 | 372 | 0.99 | Косметика и аксесуары | 1 | 6 | 6 | 5 | 4528.0 | 5186.5 | 5208.6 | 9 | 10 |
| 287 | 215639 | Снизилась | премиум | да | 2.7 | 5 | 845 | 0.32 | Товары для детей | 2 | 2 | 5 | 4 | 4379.0 | 5730.5 | 6957.7 | 6 | 10 |
| 593 | 215945 | Прежний уровень | премиум | да | 4.0 | 3 | 1007 | 0.23 | Кухонная посуда | 2 | 3 | 6 | 7 | 5004.0 | 4420.5 | 4044.8 | 19 | 23 |
| 79 | 215431 | Снизилась | премиум | да | 3.9 | 4 | 666 | 0.21 | Кухонная посуда | 3 | 3 | 6 | 7 | 4692.0 | 5455.5 | 6184.8 | 12 | 14 |
| 1263 | 216615 | Прежний уровень | стандарт | да | 5.1 | 4 | 780 | 0.27 | Домашний текстиль | 4 | 1 | 4 | 4 | 5360.0 | 5051.0 | 4913.2 | 11 | 12 |
(1296, 18)
В ходе объединения таблиц было созданы отдельны столбецы для каждого периода по выручке и времени.
Корреляционный анализ¶
Для корреляционного анализа и впоследствии модели машинного обучения, признак id не нужен, т.к. не несет никакой смысловой нагрузки.
market_full_wthout_id = market_full.drop('id', axis=1)
col_names_corr = market_full_wthout_id.select_dtypes(include='number').columns.to_list()
big_data_corr = market_full_wthout_id.phik_matrix(interval_cols=col_names_corr)
plt.figure(figsize=(14, 10))
sns.heatmap(big_data_corr, annot=True, fmt=".2f", center=0)
plt.title('Матрица корреляций phik')
plt.show()
Вывод по корреляционному анализу
Мультиколлеанарности между признаками не замечено;
В ходе анализа Phik матрицы корреляций были определены силы связи целевого признака покупательская_активность со входными, используя шкалу Чеддока:
- Высокая связь целевого признака прослеживается с входным признаком
страниц_за_визит(0.75); - Средняя связь целевого признака прослеживается с входными признаками:
маркет_актив_6_мес(0.54),акционные_покупки(0.51),средний_просмотр_категорий_за_визит(0.54),неоплаченные_продукты_штук_квартал(0.51),страниц_за_визит(0.75),выручка_препредыдущий_месяц(0.5),минут_предыдущий_месяц(0.69),минут_текущий_месяц(0.58);
- Слабая связь целевого признака прослеживается с входным признаком
популярная_категория(0.3); - Входные признаки:
маркет_актив_тек_месиошибка_сервиса, имеют нулевую корреляцию с целевым признакомпокупательская_активность. Это может указывать на то, что эти признаки не влияют на покупательскую активность. - С остальными входными признаками связь очень слабая.
Использование пайплайнов¶
df_ml = market_full.copy()
df_ml.sample(5)
| id | покупательская_активность | тип_сервиса | разрешить_сообщать | маркет_актив_6_мес | маркет_актив_тек_мес | длительность | акционные_покупки | популярная_категория | средний_просмотр_категорий_за_визит | неоплаченные_продукты_штук_квартал | ошибка_сервиса | страниц_за_визит | выручка_препредыдущий_месяц | выручка_предыдущий_месяц | выручка_текущий_месяц | минут_предыдущий_месяц | минут_текущий_месяц | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 96 | 215448 | Снизилась | премиум | да | 4.6 | 4 | 974 | 0.94 | Домашний текстиль | 4 | 3 | 3 | 7 | 4670.0 | 5351.0 | 6129.7 | 9 | 10 |
| 899 | 216251 | Прежний уровень | стандарт | да | 4.0 | 4 | 320 | 0.24 | Мелкая бытовая техника и электроника | 5 | 4 | 3 | 8 | 4645.0 | 4565.5 | 4626.6 | 10 | 9 |
| 8 | 215358 | Снизилась | стандарт | да | 4.7 | 4 | 450 | 0.13 | Домашний текстиль | 4 | 2 | 6 | 4 | 4727.0 | 3488.0 | 4209.5 | 14 | 10 |
| 704 | 216056 | Прежний уровень | премиум | да | 4.6 | 4 | 871 | 0.39 | Мелкая бытовая техника и электроника | 5 | 2 | 2 | 10 | 5442.0 | 4636.5 | 4068.4 | 10 | 16 |
| 1175 | 216527 | Прежний уровень | стандарт | нет | 4.9 | 3 | 679 | 0.15 | Техника для красоты и здоровья | 4 | 1 | 1 | 14 | 5023.0 | 4668.0 | 5106.1 | 12 | 19 |
Выделим целевой признак покупательская_активность
X = df_ml.drop(['покупательская_активность', 'id'], axis=1)
y = df_ml['покупательская_активность']
# Разделяем данные на обучающую и тестовую выборки
X_train, X_test, y_train, y_test = train_test_split(
X, # Входные признаки
y, # Целевые признаки
test_size=TEST_SIZE, # Размер тестовой выборки
random_state=RANDOM_STATE, # Случайное состояние для воспроизводимости
stratify=y
)
# Кодировка целевого признака
le = LabelEncoder()
y_train = le.fit_transform(y_train)
y_test = le.transform(y_test)
# Определение числовых и текстовых признаков
num_columns = X_train.select_dtypes(include=['int64', 'float64']).columns.tolist()
ohe_columns = X_train.select_dtypes(include=['object']).columns.tolist()
ohe_columns.remove('тип_сервиса')
ord_columns = ['тип_сервиса']
# создаём пайплайн для подготовки признаков из списка ohe_columns: заполнение пропусков и OHE-кодирование
# SimpleImputer + OHE
ohe_pipe = Pipeline(
[
(
'simpleImputer_ohe',
SimpleImputer(missing_values=np.nan, strategy='most_frequent')
),
(
'ohe',
OneHotEncoder(drop='first', handle_unknown='ignore', sparse_output=False)
)
]
)
ord_pipe = Pipeline(
[
(
'simpleImputer_before_ord',
SimpleImputer(missing_values=np.nan, strategy='most_frequent')
),
(
'ord',
OrdinalEncoder(
categories=[
['стандарт', 'премиум'],
],
handle_unknown='use_encoded_value', unknown_value=np.nan
)
),
(
'simpleImputer_after_ord',
SimpleImputer(missing_values=np.nan, strategy='most_frequent')
)
]
)
# создаём общий пайплайн для подготовки данных
data_preprocessor = ColumnTransformer(
[
('ohe', ohe_pipe, ohe_columns),
('ord', ord_pipe, ord_columns),
('num', MinMaxScaler(), num_columns)
],
remainder='passthrough'
)
pipe_final = Pipeline(
[
('preprocessor', data_preprocessor),
('model', DecisionTreeClassifier(random_state=RANDOM_STATE))
]
)
param_grid = [
# словарь для модели KNeighborsClassifier()
{
# название модели
'model': [KNeighborsClassifier()],
# указываем гиперпараметр модели n_neighbors
'model__n_neighbors': range(1, 10),
# указываем список методов масштабирования
'preprocessor__num': [StandardScaler(), MinMaxScaler()]
},
# словарь для модели DecisionTreeClassifier()
{
'model': [DecisionTreeClassifier(random_state=RANDOM_STATE)],
'model__max_depth': range(1, 10),
'model__max_features': range(1, 10),
'preprocessor__num': [StandardScaler(), MinMaxScaler(), 'passthrough'],
},
# словарь для модели SVC()
{
'model': [SVC(probability=True, random_state=RANDOM_STATE)],
'model__C': [0.1, 1, 10],
'model__gamma': ['scale', 'auto', 0.1, 1],
'preprocessor__num': [StandardScaler(), MinMaxScaler(), 'passthrough']
},
# словарь для модели LogisticRegression()
{
'model': [LogisticRegression(solver='liblinear', penalty='l1', random_state=RANDOM_STATE)],
'model__C': [0.1, 1, 10],
'preprocessor__num': [StandardScaler(), MinMaxScaler(), 'passthrough']
}
]
Т.к. строк в датафрейме не много 1296, можно использовать метод GridSearchCV, он даст наиболее точный подбор гиперпараметров, т.к. пройдет по всем.
'''
models — инициализированная модель
param_grid — словарь с гиперпараметрами модели
cv — тип кросс-валидации
scoring — метрика, которую используем для выбора лучшего решения
n_jobs=-1 — подключаем к расчёту ядра процессора
'''
grid = GridSearchCV(
pipe_final,
param_grid=param_grid,
cv=5,
scoring='roc_auc',
n_jobs=-1
)
grid.fit(X_train, y_train)
print('Лучшая модель и её параметры:\n\n', grid.best_estimator_)
print ('Метрика лучшей модели на тренировочной выборке:', round(grid.best_score_, 4))
Лучшая модель и её параметры:
Pipeline(steps=[('preprocessor',
ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('simpleImputer_ohe',
SimpleImputer(strategy='most_frequent')),
('ohe',
OneHotEncoder(drop='first',
handle_unknown='ignore',
sparse_output=False))]),
['разрешить_сообщать',
'популярная_категория']),
('ord',
Pipeline(steps=[('simpleImputer_before_or...
'маркет_актив_тек_мес',
'длительность',
'акционные_покупки',
'средний_просмотр_категорий_за_визит',
'неоплаченные_продукты_штук_квартал',
'ошибка_сервиса',
'страниц_за_визит',
'выручка_препредыдущий_месяц',
'выручка_предыдущий_месяц',
'выручка_текущий_месяц',
'минут_предыдущий_месяц',
'минут_текущий_месяц'])])),
('model',
SVC(C=0.1, gamma=0.1, probability=True, random_state=42))])
Метрика лучшей модели на тренировочной выборке: 0.9121
Для данной задачи классификации была выбрана метрика качества ROC-AUC обладающая рядом преимуществ:
- В отличие от точности (accuracy), которая зависит от фиксированного порога, ROC-AUC анализирует все возможные значения порога;
- ROC-AUC является устойчивой метрикой при наличии дисбаланса классов (например, метрики точности или F1-меры могут быть смещены в сторону большинства).
Лучшей моделью является SVC (C=0.1, gamma=0.1, probability=True, random_state=42) и ядром rbf (он используется по стандарту если не указывать другие в пайплайне), количественные данные которой были закодированы StandardScaler(), а категориальные OneHotEncoder() и OrdinalEncoder().
Метрика ROC-AUC лучшей модели на тренировочной выборке (0.9121): Это показывает, что модель хорошо обучилась на тренировочных данных и смогла уловить большую часть закономерностей в данных.
# Предсказание на тестовой выборке
y_pred = grid.predict(X_test)
# Проверка на наличие метода predict_proba
if hasattr(grid.best_estimator_['model'], 'predict_proba'):
# Предсказание вероятностей классов
proba = grid.predict_proba(X_test)[:, 1]
# Вычисление ROC-AUC на вероятностях
roc_auc = roc_auc_score(y_test, proba)
print(f'Метрика ROC-AUC на тестовой выборке: {roc_auc:.4f}')
else:
print("Метод predict_proba не поддерживается для лучшей модели.")
Метрика ROC-AUC на тестовой выборке: 0.9112
Преобразуем признак акционные_покупки в категориальный (0 - акционные_покупки <= 0.65, 1 - акционные_покупки > 0.65).
И посмотрим как изменится метрика качества.
df_ml_2 = df_ml.copy()
df_ml_2['акционные_покупки'] = df_ml_2['акционные_покупки'].apply(lambda x: 'более_0.65' if x > 0.65 else 'менее_0.65')
df_ml_2.drop('id', axis=1, inplace=True)
df_ml_2.head()
| покупательская_активность | тип_сервиса | разрешить_сообщать | маркет_актив_6_мес | маркет_актив_тек_мес | длительность | акционные_покупки | популярная_категория | средний_просмотр_категорий_за_визит | неоплаченные_продукты_штук_квартал | ошибка_сервиса | страниц_за_визит | выручка_препредыдущий_месяц | выручка_предыдущий_месяц | выручка_текущий_месяц | минут_предыдущий_месяц | минут_текущий_месяц | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Снизилась | премиум | да | 4.4 | 4 | 819 | более_0.65 | Товары для детей | 4 | 4 | 2 | 5 | 4472.0 | 5216.0 | 4971.6 | 12 | 10 |
| 1 | Снизилась | стандарт | нет | 4.9 | 3 | 539 | менее_0.65 | Домашний текстиль | 5 | 2 | 1 | 5 | 4826.0 | 5457.5 | 5058.4 | 8 | 13 |
| 2 | Снизилась | стандарт | да | 3.2 | 5 | 896 | более_0.65 | Товары для детей | 5 | 0 | 6 | 4 | 4793.0 | 6158.0 | 6610.4 | 11 | 13 |
| 3 | Снизилась | стандарт | нет | 5.1 | 3 | 1064 | более_0.65 | Товары для детей | 3 | 2 | 3 | 2 | 4594.0 | 5807.5 | 5872.5 | 8 | 11 |
| 4 | Снизилась | стандарт | да | 3.3 | 4 | 762 | менее_0.65 | Домашний текстиль | 4 | 1 | 1 | 4 | 5124.0 | 4738.5 | 5388.5 | 10 | 10 |
col_names_corr = df_ml_2.select_dtypes(include='number').columns.to_list()
col_names_corr
['маркет_актив_6_мес', 'маркет_актив_тек_мес', 'длительность', 'средний_просмотр_категорий_за_визит', 'неоплаченные_продукты_штук_квартал', 'ошибка_сервиса', 'страниц_за_визит', 'выручка_препредыдущий_месяц', 'выручка_предыдущий_месяц', 'выручка_текущий_месяц', 'минут_предыдущий_месяц', 'минут_текущий_месяц']
big_data_corr = df_ml_2.phik_matrix(interval_cols=col_names_corr)
plt.figure(figsize=(14, 10))
sns.heatmap(big_data_corr, annot=True, fmt=".2f", center=0)
plt.title('Матрица корреляций phik для модели df_ml_2')
plt.show()
Мультиколлинеарности после перевода входного признака акционные_покупки в категориальный - не обнаружено
X_new = df_ml_2.drop(['покупательская_активность'], axis=1)
y_new = df_ml_2['покупательская_активность']
# Разделяем данные на обучающую и тестовую выборки
X_train_new, X_test_new, y_train_new, y_test_new = train_test_split(
X_new, # Входные признаки
y_new, # Целевые признаки
test_size=TEST_SIZE, # Размер тестовой выборки
random_state=RANDOM_STATE, # Случайное состояние для воспроизводимости
stratify=y_new
)
# Кодировка целевого признака
y_train_new = le.transform(y_train_new)
y_test_new = le.transform(y_test_new)
# Определение числовых и текстовых признаков
num_columns = X_train_new.select_dtypes(include=['int64', 'float64']).columns.tolist()
ohe_columns = X_train_new.select_dtypes(include=['object']).columns.tolist()
ohe_columns.remove('тип_сервиса')
ord_columns = ['тип_сервиса']
# создаём общий пайплайн для подготовки данных
data_preprocessor_2 = ColumnTransformer(
[
('ohe', ohe_pipe, ohe_columns),
('ord', ord_pipe, ord_columns),
('num', MinMaxScaler(), num_columns)
],
remainder='passthrough'
)
pipe_final_2 = Pipeline(
[
('preprocessor', data_preprocessor_2),
('model', DecisionTreeClassifier(random_state=RANDOM_STATE))
]
)
'''
models — инициализированная модель
param_grid — словарь с гиперпараметрами модели
cv — тип кросс-валидации
scoring — метрика, которую используем для выбора лучшего решения
n_jobs=-1 — подключаем к расчёту ядра процессора
'''
grid_2 = GridSearchCV(
pipe_final_2,
param_grid=param_grid,
cv=5,
scoring='roc_auc',
n_jobs=-1
)
grid_2.fit(X_train_new, y_train_new)
print('Лучшая модель и её параметры:\n\n', grid_2.best_estimator_)
print ('Метрика лучшей модели на тренировочной выборке:', round(grid_2.best_score_, 4))
Лучшая модель и её параметры:
Pipeline(steps=[('preprocessor',
ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('simpleImputer_ohe',
SimpleImputer(strategy='most_frequent')),
('ohe',
OneHotEncoder(drop='first',
handle_unknown='ignore',
sparse_output=False))]),
['разрешить_сообщать',
'акционные_покупки',
'популярная_категория']),
('ord',
Pipeline(steps=[('sim...
['маркет_актив_6_мес',
'маркет_актив_тек_мес',
'длительность',
'средний_просмотр_категорий_за_визит',
'неоплаченные_продукты_штук_квартал',
'ошибка_сервиса',
'страниц_за_визит',
'выручка_препредыдущий_месяц',
'выручка_предыдущий_месяц',
'выручка_текущий_месяц',
'минут_предыдущий_месяц',
'минут_текущий_месяц'])])),
('model',
SVC(C=1, gamma=0.1, probability=True, random_state=42))])
Метрика лучшей модели на тренировочной выборке: 0.9081
Лучшей моделью также является SVC (C=0.1, gamma=0.1, probability=True, random_state=42) и ядром rbf, количественные данные которой были закодированы StandardScaler(), а категориальные OneHotEncoder() и OrdinalEncoder().
Метрика ROC-AUC лучшей модели на тренировочной выборке (0.9081): это чуть меньше, чем у предыдущей.
# Предсказание на тестовой выборке
y_pred_new = grid_2.predict(X_test_new)
# Проверка на наличие метода predict_proba
if hasattr(grid_2.best_estimator_['model'], 'predict_proba'):
# Предсказание вероятностей классов
proba_new = grid_2.predict_proba(X_test_new)[:, 1]
# Вычисление ROC-AUC на вероятностях
roc_auc_new = roc_auc_score(y_test_new, proba_new)
print(f'Метрика ROC-AUC на тестовой выборке: {roc_auc_new:.4f}')
else:
print("Метод predict_proba не поддерживается для лучшей модели.")
Метрика ROC-AUC на тестовой выборке: 0.9129
Сравнение метрики качества моделей на тренировочной и тестовой выборке при категориальном и количественном признаке акционные_покупки
data = {
'акционные_покупки_num': [round(grid.best_score_, 4), round(roc_auc, 4)],
'акционные_покупки_cat': [round(grid_2.best_score_, 4), round(roc_auc_new, 4)],
}
table = pd.DataFrame(data, index=['train', 'test'])
table
| акционные_покупки_num | акционные_покупки_cat | |
|---|---|---|
| train | 0.9121 | 0.9081 |
| test | 0.9112 | 0.9129 |
На тренировочных данных лучшую метрику ROC-AUC (0.9112) показала SVC модель с количественным входным признаком акционные_покупки, а на тестовых SVC модель с категориальным входным признаком акционные_покупки.
Принимая факт, что наивысшая и наиболее важная метрика, та которая показана на тренировочных данных.
Поэтому для дальнейшего иследования выберем первую модель с SVC модель с количественным входным признаком акционные_покупки.
Обший вывод по работе с пайплайнами
В результате выполнения моделирования, была выбрана лучшая модель и её параметры. Лучшей моделью является - SVC (C=0.1, gamma=0.1, probability=True, random_state=42) и ядром rbf, количественные данные которой были закодированы StandardScaler(), а категориальные OneHotEncoder() и OrdinalEncoder().
Метрика ROC-AUC лучшей модели на тренировочной выборке (0.9121): Это показывает, что модель хорошо обучилась на тренировочных данных и смогла уловить большую часть закономерностей в данных.
Метрика ROC-AUC на тестовой выборке (0.9112).
Таким образом, выбранная модель показала хорошие результаты и может быть использована для прогнозирования покупательской активности на основе предоставленных данных.
Анализ важности признаков¶
# Преобразуем тренировочные данные
X_train_enc = grid.best_estimator_['preprocessor'].fit_transform(X_train)
# Получаем модель
model = grid.best_estimator_['model']
# Используем shap.sample для выборки подмножества данных
sample_data = shap.sample(X_train_enc, 100) # выбираем 100 случайных примеров
# Создаем объяснитель SHAP с PermutationExplainer
explainer = shap.PermutationExplainer(model.predict_proba, sample_data)
# Преобразуем тестовые данные
X_test_enc = grid.best_estimator_['preprocessor'].transform(X_test)
# Получаем имена признаков
feature_names = grid.best_estimator_['preprocessor'].get_feature_names_out()
# Создаем DataFrame для удобства анализа
X_test_enc = pd.DataFrame(X_test_enc, columns=feature_names)
# Вычисляем SHAP-значения
shap_values = explainer.shap_values(X_test_enc)
PermutationExplainer explainer: 325it [02:47, 1.85it/s]
shap_values_class_1 = shap.Explanation(
values=shap_values[:, :, 1], # SHAP-значения для второго класса
feature_names=X_test_enc.columns, # Имена признаков
data=X_test_enc.values # Исходные данные
)
# Визуализируем важность признаков для второго класса
shap.plots.bar(shap_values_class_1, max_display=30)
shap.plots.beeswarm(shap_values_class_1, max_display=30)
На диаграмме beeswarm показана важность разных признаков для модели, которая предсказывает покупательскую активность. Признаки с положительными значениями SHAP повышают вероятность того, что покупательская активность снизится (класс 1), а признаки с отрицательными значениями - понижают (класс 0).
Вывод:
Входные признаки, num__страниц_за_визит, num__минут_предыдущий_месяц, num__минут_текущий_месяц и num__акционные_покупки, являются наиболее значимыми, так как они имеют наибольшее влияние на покупательскую_активность.
Входные признаки, ohe__популярная_категория..., ohe__тип_сервиса_стандарт и ohe__разрешить_сообщать_нет не имеют влияние и могут быть менее важными для модели.
Данные по значимости признаков помогут определить бизнесу какие факторы влияют на сохранение активности покупателя на сайте. Так из данных выше можно сделать вывод, что время проведенное на сайте сильнее всего влияет на активность клиента, а следовательно и на прибыль магазина. Это можно объяснить тем, что у пользователя есть большой выбор товаров для покупки. И ему не нужно искать другие способы покупки необходимого ему товара (например на другом сайте).
Сегментация покупателей¶
# Преобразуем predictions_train и predictions_test в pandas Series
predictions_train = pd.Series(grid.predict_proba(X_train)[:, 1], index=X_train.index)
predictions_test = pd.Series(proba_new, index=X_test.index)
# Объединяем вероятности вдоль строк (по индексу)
predictions_final = pd.concat([predictions_train, predictions_test])
# Создаём копию исходного DataFrame, который учавствовал в обучении модели
df_full = df_ml.copy()
# Проверяем совпадение длины перед добавлением нового столбца
if len(predictions_final) == len(df_full):
df_full['вероятность_снижения_активности'] = predictions_final
else:
raise ValueError("Длина predictions_final не совпадает с количеством строк в df_full.")
# Отображаем примеры итогового DataFrame
df_full.sample(5)
| id | покупательская_активность | тип_сервиса | разрешить_сообщать | маркет_актив_6_мес | маркет_актив_тек_мес | длительность | акционные_покупки | популярная_категория | средний_просмотр_категорий_за_визит | неоплаченные_продукты_штук_квартал | ошибка_сервиса | страниц_за_визит | выручка_препредыдущий_месяц | выручка_предыдущий_месяц | выручка_текущий_месяц | минут_предыдущий_месяц | минут_текущий_месяц | вероятность_снижения_активности | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 145 | 215497 | Снизилась | премиум | да | 5.6 | 5 | 719 | 0.28 | Мелкая бытовая техника и электроника | 1 | 2 | 7 | 1 | 4475.0 | 5871.5 | 6277.1 | 9 | 5 | 0.959532 |
| 151 | 215503 | Снизилась | стандарт | да | 2.4 | 5 | 638 | 0.24 | Домашний текстиль | 1 | 5 | 2 | 1 | 4119.0 | 5046.0 | 4886.1 | 7 | 8 | 0.988212 |
| 343 | 215695 | Снизилась | стандарт | да | 4.3 | 4 | 324 | 0.14 | Техника для красоты и здоровья | 2 | 7 | 4 | 4 | 5160.0 | 5314.0 | 5310.1 | 18 | 20 | 0.492313 |
| 201 | 215553 | Снизилась | стандарт | да | 0.9 | 4 | 360 | 0.33 | Домашний текстиль | 2 | 3 | 3 | 5 | 4309.0 | 4138.0 | 4594.3 | 6 | 10 | 0.968069 |
| 1177 | 216529 | Прежний уровень | стандарт | нет | 3.5 | 3 | 757 | 0.25 | Товары для детей | 3 | 1 | 2 | 7 | 4974.0 | 4340.0 | 5019.9 | 11 | 17 | 0.103883 |
# Выполняем объединение с использованием merge
df_full = df_full.merge(money, on='id', how='inner')
df_full.sample(5)
| id | покупательская_активность | тип_сервиса | разрешить_сообщать | маркет_актив_6_мес | маркет_актив_тек_мес | длительность | акционные_покупки | популярная_категория | средний_просмотр_категорий_за_визит | неоплаченные_продукты_штук_квартал | ошибка_сервиса | страниц_за_визит | выручка_препредыдущий_месяц | выручка_предыдущий_месяц | выручка_текущий_месяц | минут_предыдущий_месяц | минут_текущий_месяц | вероятность_снижения_активности | прибыль | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1219 | 216571 | Прежний уровень | стандарт | нет | 4.3 | 4 | 445 | 0.30 | Техника для красоты и здоровья | 2 | 2 | 4 | 19 | 4588.0 | 4772.5 | 5062.4 | 14 | 20 | 0.392687 | 2.27 |
| 1089 | 216441 | Прежний уровень | стандарт | нет | 4.1 | 4 | 136 | 0.30 | Мелкая бытовая техника и электроника | 3 | 4 | 3 | 10 | 4485.0 | 4356.0 | 4374.2 | 20 | 14 | 0.091297 | 1.87 |
| 722 | 216074 | Прежний уровень | премиум | да | 3.7 | 3 | 828 | 0.14 | Товары для детей | 3 | 3 | 3 | 7 | 5362.0 | 5094.0 | 5456.7 | 18 | 12 | 0.091238 | 5.90 |
| 948 | 216300 | Прежний уровень | стандарт | да | 3.7 | 4 | 714 | 0.26 | Техника для красоты и здоровья | 6 | 3 | 3 | 9 | 5039.0 | 5121.0 | 5179.5 | 14 | 16 | 0.034554 | 3.98 |
| 211 | 215563 | Снизилась | премиум | да | 3.9 | 5 | 956 | 0.35 | Косметика и аксесуары | 2 | 2 | 6 | 7 | 4985.0 | 6036.5 | 6114.5 | 10 | 9 | 0.840592 | 6.88 |
Преобразуем столбец акционные_покупки в категориальный признак для упрощения исследования категории клиентов часто покупающих по скидкам
df_full['акционные_покупки'] = df_full['акционные_покупки'].apply(lambda x: 'более_0.65' if x > 0.65 else 'менее_0.65')
df_full.sample(5)
| id | покупательская_активность | тип_сервиса | разрешить_сообщать | маркет_актив_6_мес | маркет_актив_тек_мес | длительность | акционные_покупки | популярная_категория | средний_просмотр_категорий_за_визит | неоплаченные_продукты_штук_квартал | ошибка_сервиса | страниц_за_визит | выручка_препредыдущий_месяц | выручка_предыдущий_месяц | выручка_текущий_месяц | минут_предыдущий_месяц | минут_текущий_месяц | вероятность_снижения_активности | прибыль | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1142 | 216494 | Прежний уровень | премиум | да | 5.1 | 3 | 1041 | менее_0.65 | Кухонная посуда | 2 | 0 | 2 | 4 | 5428.0 | 4820.5 | 4424.0 | 11 | 18 | 0.173000 | 5.10 |
| 1238 | 216590 | Прежний уровень | премиум | да | 6.6 | 5 | 740 | менее_0.65 | Мелкая бытовая техника и электроника | 4 | 2 | 6 | 14 | 5446.0 | 5566.0 | 5247.5 | 19 | 14 | 0.091249 | 3.63 |
| 1027 | 216379 | Прежний уровень | премиум | да | 3.3 | 4 | 827 | менее_0.65 | Домашний текстиль | 4 | 2 | 4 | 14 | 4606.0 | 5105.5 | 5668.4 | 13 | 13 | 0.077012 | 3.68 |
| 1158 | 216510 | Прежний уровень | премиум | да | 4.9 | 5 | 723 | более_0.65 | Мелкая бытовая техника и электроника | 2 | 4 | 6 | 9 | 5290.0 | 5821.0 | 6411.2 | 20 | 12 | 0.191286 | 3.76 |
| 754 | 216106 | Прежний уровень | стандарт | да | 4.4 | 5 | 360 | менее_0.65 | Товары для детей | 4 | 4 | 3 | 11 | 4454.0 | 3865.0 | 4739.9 | 14 | 12 | 0.113733 | 3.46 |
Попытаемся выделить сегмент покупателей с высокой вероятностью снижения активности и исследовать его
df_full['спад_активности'] = df_full['вероятность_снижения_активности'].apply(lambda x: 'уходящий_клиент' if x >= 0.75 else 'обычный_клиент')
df_low_act = df_full.query('спад_активности == "уходящий_клиент"')
display(df_low_act.shape)
df_low_act.sample(5)
(365, 21)
| id | покупательская_активность | тип_сервиса | разрешить_сообщать | маркет_актив_6_мес | маркет_актив_тек_мес | длительность | акционные_покупки | популярная_категория | средний_просмотр_категорий_за_визит | ... | ошибка_сервиса | страниц_за_визит | выручка_препредыдущий_месяц | выручка_предыдущий_месяц | выручка_текущий_месяц | минут_предыдущий_месяц | минут_текущий_месяц | вероятность_снижения_активности | прибыль | спад_активности | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 111 | 215463 | Снизилась | стандарт | нет | 3.1 | 4 | 472 | более_0.65 | Домашний текстиль | 2 | ... | 3 | 3 | 4470.0 | 5196.0 | 5015.3 | 8 | 12 | 0.995942 | 3.08 | уходящий_клиент |
| 116 | 215468 | Снизилась | стандарт | да | 2.7 | 4 | 515 | более_0.65 | Косметика и аксесуары | 1 | ... | 2 | 2 | 4698.0 | 6060.0 | 6346.0 | 9 | 11 | 0.988464 | 4.62 | уходящий_клиент |
| 137 | 215489 | Снизилась | стандарт | нет | 3.6 | 5 | 344 | более_0.65 | Товары для детей | 3 | ... | 4 | 4 | 4817.0 | 6183.5 | 6594.3 | 9 | 8 | 0.969915 | 3.68 | уходящий_клиент |
| 75 | 215427 | Снизилась | стандарт | нет | 2.4 | 4 | 186 | менее_0.65 | Косметика и аксесуары | 3 | ... | 7 | 6 | 4795.0 | 5549.5 | 6427.0 | 9 | 14 | 0.902136 | 2.76 | уходящий_клиент |
| 311 | 215663 | Снизилась | стандарт | да | 2.6 | 4 | 302 | менее_0.65 | Товары для детей | 1 | ... | 5 | 4 | 4291.0 | 5303.5 | 5482.9 | 5 | 6 | 0.989677 | 3.69 | уходящий_клиент |
5 rows × 21 columns
Проведем исследовательский анализ группы уходящий_клиент (аналогично тому что проводили в 3 пункте)
# Лист с названиями столбцов категориальных признаков
dict_df_full_cat = df_full.select_dtypes(include=['object']).columns.to_list()
dict_df_full_cat = [col for col in dict_df_full_cat if col not in ['спад_активности', 'покупательская_активность']]
for col in dict_df_full_cat:
show_cat_variable_by_target(df_full, col, col, 'спад_активности')
print('=' * TERM_SIZE)
====================================================================================================================================================================================
====================================================================================================================================================================================
====================================================================================================================================================================================
====================================================================================================================================================================================
Клиенты с высокой вероятностью снижения активности, чаще остальных покупают товары по скидке.
Попробуем провести анализ данной группы клиентов. Предварительно поместив таких покупателей в отдельный сегмент уходящий_скидочник.
df_full['уходящий_скидочник'] = df_full.apply(
lambda row: 'да' if row['спад_активности'] == "уходящий_клиент" and row['акционные_покупки'] == "более_0.65" else 'нет',
axis=1
)
df_full.query('уходящий_скидочник == "да"')
| id | покупательская_активность | тип_сервиса | разрешить_сообщать | маркет_актив_6_мес | маркет_актив_тек_мес | длительность | акционные_покупки | популярная_категория | средний_просмотр_категорий_за_визит | ... | страниц_за_визит | выручка_препредыдущий_месяц | выручка_предыдущий_месяц | выручка_текущий_месяц | минут_предыдущий_месяц | минут_текущий_месяц | вероятность_снижения_активности | прибыль | спад_активности | уходящий_скидочник | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 215349 | Снизилась | премиум | да | 4.4 | 4 | 819 | более_0.65 | Товары для детей | 4 | ... | 5 | 4472.0 | 5216.0 | 4971.6 | 12 | 10 | 0.964315 | 4.16 | уходящий_клиент | да |
| 3 | 215352 | Снизилась | стандарт | нет | 5.1 | 3 | 1064 | более_0.65 | Товары для детей | 3 | ... | 2 | 4594.0 | 5807.5 | 5872.5 | 8 | 11 | 0.947265 | 4.21 | уходящий_клиент | да |
| 13 | 215364 | Снизилась | премиум | да | 4.3 | 4 | 708 | более_0.65 | Домашний текстиль | 3 | ... | 3 | 4942.0 | 5795.5 | 5484.8 | 11 | 9 | 0.994606 | 2.67 | уходящий_клиент | да |
| 14 | 215365 | Снизилась | стандарт | да | 3.9 | 4 | 167 | более_0.65 | Техника для красоты и здоровья | 6 | ... | 5 | 4190.0 | 4577.0 | 4799.3 | 6 | 10 | 0.810166 | 3.65 | уходящий_клиент | да |
| 22 | 215373 | Снизилась | премиум | нет | 3.8 | 3 | 811 | более_0.65 | Товары для детей | 2 | ... | 3 | 4293.0 | 4632.0 | 5161.1 | 10 | 8 | 0.993379 | 3.69 | уходящий_клиент | да |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 393 | 215745 | Снизилась | стандарт | да | 1.7 | 4 | 550 | более_0.65 | Мелкая бытовая техника и электроника | 4 | ... | 6 | 4990.0 | 5654.5 | 6126.4 | 9 | 9 | 0.897747 | 5.10 | уходящий_клиент | да |
| 395 | 215747 | Снизилась | стандарт | да | 3.5 | 5 | 452 | более_0.65 | Товары для детей | 1 | ... | 4 | 4168.0 | 3555.0 | 4089.3 | 10 | 10 | 0.975485 | 3.32 | уходящий_клиент | да |
| 490 | 215842 | Снизилась | премиум | да | 4.3 | 3 | 1036 | более_0.65 | Товары для детей | 4 | ... | 9 | 4642.0 | 4408.0 | 4740.8 | 8 | 10 | 0.941479 | 1.33 | уходящий_клиент | да |
| 558 | 215910 | Снизилась | стандарт | нет | 3.9 | 3 | 509 | более_0.65 | Косметика и аксесуары | 2 | ... | 5 | 4874.0 | 5360.0 | 5911.2 | 11 | 17 | 0.948036 | 4.61 | уходящий_клиент | да |
| 1252 | 216604 | Прежний уровень | стандарт | да | 4.9 | 5 | 350 | более_0.65 | Домашний текстиль | 3 | ... | 5 | 4735.0 | 4545.0 | 4840.1 | 14 | 14 | 0.750895 | 2.88 | уходящий_клиент | да |
114 rows × 22 columns
Получили 114 клиентов удволетворяющих критерию уходящий_скидочник.
# Формирование списка столбцов с количественными признаками
num_variables_col = df_full.select_dtypes(include=['number']).columns.to_list()
num_variables_col
['id', 'маркет_актив_6_мес', 'маркет_актив_тек_мес', 'длительность', 'средний_просмотр_категорий_за_визит', 'неоплаченные_продукты_штук_квартал', 'ошибка_сервиса', 'страниц_за_визит', 'выручка_препредыдущий_месяц', 'выручка_предыдущий_месяц', 'выручка_текущий_месяц', 'минут_предыдущий_месяц', 'минут_текущий_месяц', 'вероятность_снижения_активности', 'прибыль']
num_variables_col = [col for col in num_variables_col if col not in ['id', 'вероятность_снижения_активности']]
# Вывод графиков для датафрейма market_file
for col in num_variables_col:
show_num_variable(df_full, col, 'уходящий_скидочник')
print('=' * TERM_SIZE)
====================================================================================================================================================================================
====================================================================================================================================================================================
====================================================================================================================================================================================
====================================================================================================================================================================================
====================================================================================================================================================================================
====================================================================================================================================================================================
====================================================================================================================================================================================
====================================================================================================================================================================================
====================================================================================================================================================================================
====================================================================================================================================================================================
====================================================================================================================================================================================
====================================================================================================================================================================================
====================================================================================================================================================================================
Скидочники проводят меньше времени на сайте, чем другие покупатели. Вероятно они отслеживают цену определенного необходимого им товара;
Так же они чаще неоплачивают товары в корзине. Скорее всего они помещают нужные им товары в корзину (для более быстрого поиска в дальнейшем) и ждут их скидки;
Скидочники в среднем приносят магазину такую же прибыль как и другие покупатели, поэтому эта категория важна магазину и с ней необходимо считаться.
# Лист с названиями столбцов категориальных признаков
dict_df_full_cat = df_full.select_dtypes(include=['object']).columns.to_list()
dict_df_full_cat
['покупательская_активность', 'тип_сервиса', 'разрешить_сообщать', 'акционные_покупки', 'популярная_категория', 'спад_активности', 'уходящий_скидочник']
dict_df_full_cat = [col for col in dict_df_full_cat if col not in ['уходящий_скидочник', 'покупательская_активность', 'акционные_покупки', 'спад_активности']]
for col in dict_df_full_cat:
show_cat_variable_by_target(df_full, col, col, 'уходящий_скидочник')
print('=' * TERM_SIZE)
====================================================================================================================================================================================
====================================================================================================================================================================================
====================================================================================================================================================================================
stats_df = df_full.groupby('уходящий_скидочник')[num_variables_col].describe().round(3).T
try:
stats_df.to_excel('output_2.xlsx')
except:
display(stats_df)
Общий вывод:
На основе данных смоделированной модели был выделен сегмент покупателей под названием уходящий_скидочник (Клиенты с высокой вероятностью снижения активности, часто покупающие товары по скидке);
Уходящий_скидочник чаще пользуются типом сервиса премиум, возможно это связано с увеличенной скидкой или большим числом товаров находящихся по акции;
Так же уходящий_скидочник чаще других покупает товары для детей.
На основе этих наблюдений были сделаны следующие предложения для работы с сегментом уходящий_скидочник:
- Увеличение количества акций и скидок.
- Улучшение системы рекомендаций.
- Работа с неоплаченными продуктами через специальные скидки или напоминания.
- Создание персонализированных предложений на основе данных о популярных категориях покупателя.
- Использование рассылки для отправки персонализированных предложений и информировании о проходящих акциях.
- Проведение более частых акций для категории товаров - товары для детей.
- Т.к. данный сегмент покупателей чаще используют премиум сервис, можно специально для них добавить новую подписку с увеличенной скидкой на определенные товары.
Общий вывод¶
- В данной работе была рассмотрена задача по разработке решения для интернет-магазина «В один клик», которое поможет увеличить покупательскую активность постоянных клиентов через персонализированные предложения. Для была предсказана вероятность снижения покупательской активности клиента в следующие три месяца, выделен сегмент покупателей
уходящий_скидочники разработаны для них персонализированные предложения. - Был проведен предварительный анализ данных в ходе которого получили:
- В датафреймах отсвутствуют пропуски;
- Название столбцов датафреймов необходимо привести к snake_case;
- Нет ошибок в типах данных в датафреймах;
- Столбцы id в датафреймах сделали значениями индексов;
- Была получена общая информация о датафреймах.
- Была проведена предобработка данных:
- Приведены названия столбцов датафреймов к snake_case;
- Проверены датафреймы на дубликаты и выявлено их отсутствие.
- Был выполнен исследовательский анализ данных (пункт 3);
- Было проведено объедение данных датафреймов: market_file, market_money и market_time, для клиентов с наличием покупок в 3-х последних месяцах;
- Был проведен корреляционный анализ данных (пункт 5);
- Была выполнена задача классификации с использованием метода пайплайнов в МО. В результате было получено:
- В результате выполнения моделирования, была выбрана лучшая модель и её параметры. Лучшей моделью стала -
SVC(C=0.1, gamma=0.1, probability=True, random_state=42) и ядромrbf, количественные данные которой были закодированы методомStandardScaler(), а категориальныеOneHotEncoder()иOrdinalEncoder(). Метрика ROC-AUC лучшей модели на тренировочной выборке (0.9121): Это показывает, что модель хорошо обучилась на тренировочных данных и смогла уловить большую часть закономерностей в данных. Метрика ROC-AUC на тестовой выборке (0.9112).
- В результате выполнения моделирования, была выбрана лучшая модель и её параметры. Лучшей моделью стала -
- Был проведен анализ влияния входных признаков на целевой (
покупательская_активность) используя метод SHAP:- Входные признаки,
num__страниц_за_визит,num__минут_предыдущий_месяциnum__минут_текущий_месяц, являются наиболее значимыми, так как они имеют наибольшее влияние напокупательскую_активность.
Входные признаки,ohe__популярная_категория...(кроме бытовой техники и электроники),ohe__тип_сервиса_стандартиohe__разрешить_сообщать_нетне имеют влияние и могут быть менее важными для модели.
Данные по значимости признаков помогут определить бизнесу какие факторы влияют на сохранение активности покупателя на сайте. Так из данных выше можно сделать вывод, что время проведенное на сайте сильнее всего влияет на активность клиента, а следовательно и на прибыль магазина. Это можно объяснить тем, что у пользователя есть большой выбор товаров для покупки. И ему не нужно искать другие способы покупки необходимого ему товара (например на другом сайте).
- Входные признаки,
- Была выполнена сегментация клиентов интернет-магазина «В один клик». Они характеризуются меньшим временем на сайте, узкой фокусировкой на товарах со скидками и большим количеством неоплаченных товаров. Для стимулирования их активности предлагается увеличить количество акций, улучшить систему рекомендаций, работать с неоплаченными товарами, создать дополнительные скидочные подписки и использовать рассылку для отправки персонализированных предложений.